#!/usr/bin/env python

#    FlyBack
#    Copyright (C) 2007 Derek Anderson
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License along
#    with this program; if not, write to the Free Software Foundation, Inc.,
#    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

import sys, traceback, math

try:
    import sqlite3
except:
    from pysqlite2 import dbapi2 as sqlite3

import dircache
import desktop
from datetime import datetime, timedelta
from time import strptime
import threading
import help_data
import config_backend
import getopt
import schema_upgrades
import progress_reporter
import misc




    
BACKUP_DATE_FORMAT = '%Y%m%d_%H%M%S'
BACKUP_DIR_DATE_FORMAT = BACKUP_DATE_FORMAT+'.backup'

class BackupDb():
    def __init__(self, config , destination_os):
        self.config = config
        self.destination_os = destination_os
        
        backup_location = self.config.get_storage_location()
        self.database_file = self.destination_os.join( backup_location, 'flyback', 'backup_db.sqlite3' )
        self._verify_db()
        
    def _verify_db(self):
        conn = sqlite3.connect( self.database_file )
        c = conn.cursor()
        
        # get our current schema version
        try:
            c.execute("select value from meta where key='schema_version'")
            schema_version = int(c.fetchone()[0])
            print 'schema_version', schema_version
        except sqlite3.OperationalError:
            schema_version = 0
            
        while True:
            best_available_upgrade = None
            best_available_upgrade_version = 0
            
            for version, upgrade in schema_upgrades.UPGRADES_SQLITE3.iteritems():
                if version[0]!=schema_version:
                    continue
                if version[1] > best_available_upgrade_version:
                    best_available_upgrade_version = version[1]
                    best_available_upgrade = upgrade
                    
            if best_available_upgrade:
                print 'upgrading your backup db from v%i to v%i' % (schema_version, best_available_upgrade_version)
                for cmd in best_available_upgrade:
                    print '\t', cmd
                    c.execute(cmd)
                conn.commit()
                schema_version = best_available_upgrade_version
            else:
                break
            
    def start_operation(self, type):
        start_time = datetime.utcnow()
        conn = sqlite3.connect( self.database_file )
        c = conn.cursor()
        c.execute( "insert into operation (type,start_time) values (?,?)", (type, start_time.strftime(BACKUP_DATE_FORMAT)) )
        conn.commit()
        c.execute( "select id from operation order by id desc" )
        self.operation_id = c.fetchone()[0]
        
        print 'starting operation', type, self.operation_id
        
        return start_time
        
    def insert_command(self, cmd, std_out, std_err):
        conn = sqlite3.connect( self.database_file )
        c = conn.cursor()
        c.execute( "insert into command (operation_id,cmd,stdout,stderr) values (?,?,?,?)", (self.operation_id, cmd, std_out, std_err) )
        conn.commit()

        
    def end_operation(self, failure=None):
        conn = sqlite3.connect( self.database_file )
        c = conn.cursor()
        c.execute( "update operation set end_time=?, failure=? where id=?", (datetime.utcnow().strftime(BACKUP_DATE_FORMAT), failure, self.operation_id) )
        conn.commit()
        print 'ending operation', self.operation_id
    
class BackupLockfile():
    def __init__(self, config , destination_os):
        self.config = config
        self.destination_os = destination_os
        
        self.lockfile_location = self.config.get_storage_location() +'/flyback'
        self.lockfile = self.lockfile_location +'/lockfile.txt'
        
    def get_lock(self):
    
        if not self.destination_os.is_path_a_directory( self.lockfile_location ):
            return ("The external storage location you've specified does not exist."
                + "  Please update your preferences.")
        
        if self.destination_os.is_path_a_file(self.lockfile):
            return ("The external storage location you've specified is already in use."
                + "  Please quit any other open instances of FlyBack"
                + " (or wait for their backups to complete) before starting a new backup.")
        
        self.destination_os.touch_file(self.lockfile)
        return None
    
    def release_lock(self):
        self.destination_os.delete_file(self.lockfile)
    


class Backup:
    
    xml = None
    progress_report = None
    parent_backup_dir = None
    included_dirs = []
    excluded_patterns = []

    def __init__(self, config, source_os, destination_os, arg_progress_reporter=None):
        self.config = config
        
        self.source_os = source_os
        self.destination_os = destination_os
        
        self.backup_db = BackupDb(config, destination_os)
        self.backup_lockfile = BackupLockfile(config, destination_os)
        
        self.progress_reporter = arg_progress_reporter
        
        if not self.progress_reporter:
            self.progress_reporter = progress_reporter.ProgressReporter()
            
    def get_available_backups(self):
        self.parent_backup_dir = self.config.get_storage_location()
        if not self.parent_backup_dir:
#            error = gtk.MessageDialog( type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_OK, flags=gtk.DIALOG_MODAL )
#            error.set_markup('Please select an external storage location in the preferences window.')
#            error.show()
            return []
        self.parent_backup_dir += '/flyback'
        try:
            dirs = dircache.listdir(self.parent_backup_dir)
            dir_datetimes = []
            for dir in dirs:
                try:
                    dir_datetimes.append( datetime(*strptime(dir, BACKUP_DIR_DATE_FORMAT)[0:6]) )
                except:
                    pass # file not a backup
            dir_datetimes.sort(reverse=True)
            return dir_datetimes
        except:
            print 'no available backups found'
#            traceback.print_stack()
            return []
        
    def get_latest_backup_dir(self):
        available_backups = self.get_available_backups()
        if available_backups:
            return available_backups[0]
        else:
            return None
    
    def get_backup_command(self, latest_backup_dir, dir, new_backup):
        options = []
        options.append('-aP')
        options.append('--delete')
        
        if self.config.get_only_one_file_system():
            options.append('--one-file-system')
            
        for x in self.excluded_patterns:
            options.append( '--exclude="%s"' % x )
            
        return "nice -n19 rsync "+ ' '.join(options) +" '%s/' '%s/'" % (dir, new_backup + dir)
    
    def run_cmd_output_gui(self, cmd, os_backend):
            
        final_out = []
        final_err = []

        self.progress_reporter.set_description('executing command ' + cmd[:30] + ' ...')
        self.progress_reporter.append_output('$ '+ cmd)
            
        stdin, stdout, stderr = os_backend.shell_command(cmd)
        
        for line in stdout:
            final_out.append(line)
            
            if line.find('(xfer') >= 0:
                remaining, total = line.split('=')[1].split(')')[0].split('/')
                total = float(total)
                completed = total - float(remaining)
                self.progress_reporter.set_progress(completed / total)
                
            else:
                self.progress_reporter.append_output( line )
                    
        for line in stderr:
            final_err.append(line)
            self.progress_reporter.append_output( line )
                
        self.progress_reporter.append_output( '' )
            
        stdin.close()
        stdout.close()
        stderr.close()
        
        self.backup_db.insert_command(cmd, ''.join(final_out), ''.join(final_err))
        
        self.progress_reporter.set_progress(0)
        self.progress_reporter.set_description('')

        return final_out
            
    def backup(self):

        msg = self.backup_lockfile.get_lock()
        
        if msg:
            self.progress_reporter.show_error("<b>External Storage Location Error</b>\n\n" + msg)
            return

        latest_backup_dir = self.get_latest_backup_dir()
        self.included_dirs = self.config.get_included_dirs()
        self.excluded_patterns = self.config.get_excluded_patters()
        
        if not self.included_dirs:
            resp = 'No directories set to backup.  Please add something to the "included dirs" list in the preferences window.'
            self.progress_reporter.show_error(resp)

        start_time = self.backup_db.start_operation('backup')
                
        try:
            new_backup = self.parent_backup_dir +'/'+ start_time.strftime(BACKUP_DIR_DATE_FORMAT)
    
            self.progress_reporter.set_backup_in_progress(True)
    
            if latest_backup_dir:
                last_backup = self.parent_backup_dir +'/'+ latest_backup_dir.strftime(BACKUP_DIR_DATE_FORMAT)
                self.run_cmd_output_gui("cp -al '%s' '%s'" % (last_backup, new_backup), self.destination_os)
                self.run_cmd_output_gui("chmod u+w '%s'" % new_backup, self.destination_os)
            
            for dir in self.included_dirs:
                self.run_cmd_output_gui("mkdir -p '%s'" % (new_backup + dir), self.destination_os)
                cmd = self.get_backup_command(latest_backup_dir, dir, new_backup)
                self.run_cmd_output_gui(cmd, self.destination_os)
                
            self.run_cmd_output_gui("chmod -w '%s'" % new_backup, self.destination_os)

            self.backup_db.end_operation()
            
        except:
            try:
                msg = '' + sys.exc_info()[0]
            except:
                msg = "An unknown backup error occured"
                
            self.progress_reporter.show_error(msg)
            self.backup_db.end_operation(msg)
        
        self.delete_too_old_backups()
        self.delete_old_backups_to_free_space()
        
        self.backup_lockfile.release_lock()

        self.progress_reporter.set_backup_in_progress(False)

    def restore(self):
        """ Restore files/directories from backup
        
        this feature is currently broken
        
        """
        
        self.progress_reporter.show_error('Restore feature is currently broken')
        
        src = self.parent_backup_dir +'/'+ self.main_gui.selected_backup.strftime(BACKUP_DIR_DATE_FORMAT) + self.main_gui.cur_dir
#        print 'src', src
        dest = self.main_gui.cur_dir
#        print 'dest', dest
        model, all_selected = self.xml.get_widget('file_list').get_selection().get_selected_rows()
#        print 'model', model

        if not all_selected:
            self.progress_reporter.show_error('<b>No files selected to restore</b>\n\n'
                                              + 'Please select something from the file list.')
            return
        
        self.progress_reporter.set_restore_in_progress(False)

        self.backup_db.start_operation('restore')
                
        if not os.path.isdir(dest):
            cmd = "mkdir -p '%s'" % dest
            self.run_cmd_output_gui(cmd, self.source_os)

        for selected in all_selected:
            self.progress_reporter.append_output('selected ' +  selected)
            self.progress_reporter.append_output('model.get ' + model.get( model.get_iter(selected), 0))

            local_file = model.get( model.get_iter(selected), 0)[0]
            file = src.rstrip('/') +'/'+ local_file
            
            print 'file', file
            if os.path.isdir(file):
                cmd = 'cp -avR "%s" "%s"' % (file, dest)
            else:
                cmd = 'cp -av "%s" "%s"' % (file, dest)
                
            file_pairs = self.run_cmd_output_gui(cmd, self.source_os)
#            for file_pair in file_pairs:
#                to_f = file_pair.split(' -> ')[1].strip("'`\n")
#                if os.path.isdir(to_f):
#                    cmd = 'chmod -R u+w "%s"' % to_f
#                else:
#                    cmd = 'chmod u+w "%s"' % to_f
#                self.run_cmd_output_gui(True,cmd)
            
        self.backup_lockfile.release_lock()
        
        self.progress_reporter.ddd(False)
        
        
    def delete_too_old_backups(self):
        if not self.config.get_autoclean_after_time():
            return
        
        pref_delete_backups_after_qty = self.config.get_autoclean_after_time_qty()
        pref_delete_backups_after_unit = self.config.get_autoclean_after_time_units()
        
        delete_before_date = None
        if pref_delete_backups_after_unit=='years':
            delete_before_date = misc.get_x_years_ago( datetime.now(), pref_delete_backups_after_qty )
            
        if pref_delete_backups_after_unit=='months':
            delete_before_date = misc.get_x_months_ago( datetime.now(), pref_delete_backups_after_qty )
            
        if pref_delete_backups_after_unit=='days':
            delete_before_date = misc.get_x_days_ago( datetime.now(), pref_delete_backups_after_qty )

        self.backup_db.start_operation('delete_too_old_backups')


        if delete_before_date:
            for x in self.get_available_backups():
                if x < delete_before_date:
                    backup_dir = self.parent_backup_dir +'/'+ x.strftime(BACKUP_DIR_DATE_FORMAT)
                    self.run_cmd_output_gui("chmod u+w '%s'" % backup_dir, self.destination_os)
                    self.run_cmd_output_gui("rm -Rf '%s'" % backup_dir, self.destination_os)
        
        self.backup_db.end_operation()
                    

    def delete_old_backups_to_free_space(self):
        if not self.config.get_autoclean_for_space():
            return

        pref_delete_backups_free_space_qty = self.config.get_autoclean_for_space_qty()
        pref_delete_backups_free_space_unit = self.config.get_autoclean_for_space_units()
        
        min_free_space = pref_delete_backups_free_space_qty
        
        if pref_delete_backups_free_space_unit=='MB':
            min_free_space *= math.pow(2,10)
            
        if pref_delete_backups_free_space_unit=='GB':
            min_free_space *= math.pow(2,20)
        
        
        available_backups = self.get_available_backups()
        available_backups.reverse()

        self.backup_db.start_operation('delete_old_backups_to_free_space')
        
        for x in available_backups:
            free_space = self.destination_os.get_free_space(self.parent_backup_dir)
            if free_space < min_free_space:
                print 'x, free_space, min_free_space', x, free_space, min_free_space
                backup_dir = self.parent_backup_dir +'/'+ x.strftime(BACKUP_DIR_DATE_FORMAT)
                self.run_cmd_output_gui("chmod u+w '%s'" % backup_dir, self.destination_os)
                self.run_cmd_output_gui("rm -Rf '%s'" % backup_dir, self.destination_os)
            else:
                break
                
        self.backup_db.end_operation()

